eletrotupi / tcc/ frontend/app/entry/[id].tsxad95ceb1d7d44db7a364485d77c18e8e3c2a4392
13.2 KB | 433 lines Raw
1
import { useState, useEffect } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11

12
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
13
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
14
import { Badge } from "@/components/ui/Badge";
15
import { Button } from "@/components/ui/Button";
16
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
17
import { Section, SectionHeader } from "@/components/ui/Sections";
18
import { Theme, Colors, Spacing } from "@/constants/theme";
19
import { getMood } from "@/constants/moods";
20
import { useMoodEntry, useMoodEntries, useDeleteMoodEntry } from "@/hooks";
21
import { formatMoment } from "@/lib/utils/time";
22
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
23
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
24
import { getTriggerCategory } from "@/constants/trigger-categories";
25
import { getCareActionMeta } from "@/constants/care-action-categories";
26

27
const impactColor = (n: number) => {
28
  if (n <= 1) return '#86efac';
29
  if (n <= 2) return '#22c55e';
30
  if (n <= 3) return '#f59e0b';
31
  if (n <= 4) return '#f97316';
32
  return '#ef4444';
33
};
34

35
export default function MoodDetailScreen() {
36
  const { id } = useLocalSearchParams<{ id: string }>();
37
  const { data: entry, isLoading, isError } = useMoodEntry(id);
38
  const { data: latestEntries } = useMoodEntries({ limit: 1 });
39
  const deleteMoodEntry = useDeleteMoodEntry();
40
  const [minutesLeft, setMinutesLeft] = useState(0);
41
  const [canDelete, setCanDelete] = useState(false);
42

43
  useEffect(() => {
44
    const updateTimer = () => {
45
      if (!entry?.moment) return;
46
      const createdAt = new Date(entry.moment);
47
      const msSinceCreation = Date.now() - createdAt.getTime();
48
      const deletionWindowMs = 5 * 60 * 1000;
49

50
      const newCanDelete = msSinceCreation < deletionWindowMs;
51
      const newMinutesLeft = Math.max(
52
        0,
53
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
54
      );
55

56
      setCanDelete(newCanDelete);
57
      setMinutesLeft(newMinutesLeft);
58
    };
59

60
    updateTimer(); // Set initial values
61

62
    const interval = setInterval(updateTimer, 1000);
63

64
    return () => clearInterval(interval);
65
  }, [entry?.moment]);
66

67
  if (isLoading) {
68
    return (
69
      <View style={styles.centered}>
70
        <ActivityIndicator size="large" color={Theme.colors.light.tint} />
71
      </View>
72
    );
73
  }
74

75
  if (isError || !entry) {
76
    return (
77
      <View style={styles.centered}>
78
        <Text style={styles.errorText}>Registro não encontrado.</Text>
79
        <Pressable onPress={() => router.back()} style={styles.backButton}>
80
          <Text style={styles.backButtonText}>Voltar</Text>
81
        </Pressable>
82
      </View>
83
    );
84
  }
85

86
  const moment = new Date(entry.moment);
87
  const moodDef = getMood(entry.selectedMood.toLowerCase());
88
  const isLatestEntry = latestEntries?.entries[0]?.id === entry.id;
89

90
  const handleDelete = async () => {
91
    await deleteMoodEntry.mutateAsync(String(entry.id));
92

93
    router.back();
94
  };
95

96
  const deleteButtonMsg = () => {
97
    const msg = "Excluir Registro";
98
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
99
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
100

101
    return `${msg} ${suffix}`;
102
  };
103

104
  const hasAnyLinks = (
105
    entry.triggerLinks && entry.triggerLinks.length > 0
106
  ) || (entry.careActions && entry.careActions.length > 0);
107

108
  return (
109
    <>
110
      <Stack.Screen options={{ title: "Ver detalhes" }} />
111

112
      <ScrollView
113
        style={styles.container}
114
        contentContainerStyle={styles.content}
115
        showsVerticalScrollIndicator={false}
116
      >
117
        {/* Header card */}
118
        <Card style={styles.headerCard}>
119
          <Between>
120
            <Row gap={12} style={{ alignItems: "center" }}>
121
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
122
              <Col gap={2}>
123
                <Text style={styles.moodLabel}>
124
                  {moodDef?.label ?? entry.selectedMood}
125
                </Text>
126
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
127
              </Col>
128
            </Row>
129
            {/* If this is the latest mood entry, show the active badge */}
130
            {isLatestEntry && <Badge label="Registro Ativo" variant="green" />}
131
          </Between>
132
        </Card>
133

134
        {/* Core levels */}
135
        <Section>
136
          <SectionHeader title="Níveis emocionais" variant="subtle" />
137
          {[
138
            { id: "energy", label: "Energia", value: entry.energyLevel },
139
            { id: "stress", label: "Estresse", value: entry.stressLevel },
140
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
141
          ].map((item) => (
142
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
143
              <View key={item.label}>
144
                <View style={styles.componentRow}>
145
                  <Text style={styles.componentLabel}>{item.label}</Text>
146
                  <Text style={styles.componentIntensityGreen}>
147
                    {item.value}/10
148
                  </Text>
149
                </View>
150

151
                <View style={styles.progressTrack}>
152
                  <View
153
                    style={[
154
                      styles.progressFill,
155
                      { width: `${(item.value / 10) * 100}%` },
156
                    ]}
157
                  />
158
                </View>
159
              </View>
160
            </Card>
161
          ))}
162
        </Section>
163

164
        <Section>
165
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
166
          <Col gap={0}>
167
            {entry.moodComponents.map((comp) => {
168
              return (
169
                <MoodComponentCard
170
                  key={comp.id}
171
                  id={comp.component.toLowerCase()}
172
                  intensity={comp.intensity}
173
                />
174
              );
175
            })}
176
          </Col>
177
        </Section>
178

179
        {/* Linked triggers */}
180
        <Section>
181
          <SectionHeader title="Vínculo" variant="subtle" />
182
          {!!hasAnyLinks ? (
183
            <Col gap={8}>
184
              {(entry.triggerLinks ?? []).map((link) => {
185
                const cat = getTriggerCategory(link.trigger.category);
186
                return (
187
                  <Pressable
188
                    key={link.id}
189
                    onPress={() => router.push(`/triggers/${link.trigger.id}`)}
190
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
191
                  >
192
                    <Card style={styles.linkCard}>
193
                      <Row style={{ alignItems: "center", gap: 12 }}>
194
                        <View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
195
                          <MaterialIcons name={cat.icon} size={22} color={cat.color} />
196
                        </View>
197
                        <Col gap={3} style={{ flex: 1 }}>
198
                          <Row style={{ alignItems: "center", gap: 6 }}>
199
                            <Text style={styles.linkLabel}>{cat.label}</Text>
200
                            <Badge label="Gatilho" variant="orange" style={undefined} />
201
                          </Row>
202
                          <Text style={styles.linkTime}>
203
                            {formatMoment(new Date(link.trigger.moment))}
204
                          </Text>
205
                        </Col>
206
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
207
                      </Row>
208
                      <View style={styles.impactBar}>
209
                        <View
210
                          style={[
211
                            styles.impactFill,
212
                            {
213
                              width: `${(link.perceivedImpact / 5) * 100}%`,
214
                              backgroundColor: impactColor(link.perceivedImpact),
215
                            },
216
                          ]}
217
                        />
218
                      </View>
219
                    </Card>
220
                  </Pressable>
221
                );
222
              })}
223

224
              {(entry.careActions ?? []).map((ca) => {
225
                const meta = getCareActionMeta(ca);
226

227
                return (
228
                  <Pressable
229
                    key={ca.id}
230
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
231
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
232
                  >
233
                    <Card style={styles.linkCard}>
234
                      <Row style={{ alignItems: 'center', gap: 12 }}>
235
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
236
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
237
                        </View>
238
                        <Col gap={3} style={{ flex: 1 }}>
239
                          <Row style={{ alignItems: 'center', gap: 6 }}>
240
                            <Text style={styles.linkLabel}>{meta.label}</Text>
241
                            <Badge label="Consulta" variant="blue" style={undefined} />
242
                          </Row>
243
                          <Text style={styles.linkTime}>
244
                            {formatMoment(new Date(ca.moment))}
245
                          </Text>
246
                        </Col>
247

248
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
249
                      </Row>
250
                    </Card>
251
                  </Pressable>
252
                );
253
              })}
254
            </Col>
255
          ) : (
256
            <NoLinkMessage />
257
          )}
258
        </Section>
259

260
        {/* Annotation / notes */}
261
        {entry.annotation && (
262
          <Section>
263
            <SectionHeader title="Anotações" variant="subtle" />
264
            <Card style={{ padding: Spacing.cardGap }}>
265
              <Text style={styles.annotation}>
266
                &quot;{entry.annotation?.trim()}&quot;
267
              </Text>
268
            </Card>
269
          </Section>
270
        )}
271

272
        {/* Delete */}
273
        <View style={styles.section}>
274
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
275
          <Button
276
            variant="danger"
277
            loading={deleteMoodEntry.isPending}
278
            disabled={!canDelete || deleteMoodEntry.isPending}
279
            onPress={handleDelete}
280
            title={deleteButtonMsg()}
281
          />
282

283
          <SubtleInfoCard
284
            style={styles.deleteInfo}
285
            text={`
286
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
287
              após o envio para garantir a integridade do histórico emocional.
288
              `}
289
          />
290
        </View>
291
      </ScrollView>
292
    </>
293
  );
294
}
295

296
const styles = StyleSheet.create({
297
  container: {
298
    flex: 1,
299
    backgroundColor: Theme.colors.light.background,
300
  },
301
  content: {
302
    padding: Spacing.containerPadding,
303
    gap: Spacing.sectionGap,
304
    paddingBottom: 48,
305
  },
306
  centered: {
307
    flex: 1,
308
    justifyContent: "center",
309
    alignItems: "center",
310
    gap: 12,
311
  },
312
  errorText: {
313
    fontSize: 15,
314
    color: Colors.light.textSecondary,
315
  },
316
  backButton: {
317
    paddingHorizontal: 20,
318
    paddingVertical: 10,
319
    backgroundColor: Theme.colors.light.tint,
320
    borderRadius: Theme.borderRadius.md,
321
  },
322
  backButtonText: {
323
    fontWeight: "700",
324
    color: Theme.colors.light.text,
325
  },
326

327
  // Header
328
  headerCard: {
329
    padding: 16,
330
  },
331
  moodIcon: {
332
    fontSize: 40,
333
  },
334
  moodLabel: {
335
    fontSize: 17,
336
    fontWeight: "700",
337
    color: Colors.light.text,
338
  },
339
  moodTime: {
340
    fontSize: 13,
341
    color: Colors.light.textSecondary,
342
  },
343

344
  // Sections
345
  section: {
346
    gap: 8,
347
  },
348
  sectionLabel: {
349
    fontSize: 11,
350
    fontWeight: "600",
351
    letterSpacing: 0.8,
352
    color: Colors.light.textSecondary,
353
    textTransform: "uppercase",
354
  },
355

356
  // Components
357
  componentRow: {
358
    flexDirection: "row",
359
    justifyContent: "space-between",
360
    alignItems: "center",
361
    paddingVertical: 10,
362
  },
363
  componentLabel: {
364
    fontSize: 15,
365
    fontWeight: "700",
366
    color: Colors.light.text,
367
  },
368
  componentIntensity: {
369
    fontSize: 13,
370
    color: Colors.light.textSecondary,
371
  },
372
  componentIntensityGreen: {
373
    fontSize: 13,
374
    fontWeight: "700",
375
    color: Theme.colors.light.tint,
376
  },
377
  progressTrack: {
378
    height: 6,
379
    backgroundColor: Theme.colors.light.divider,
380
    borderRadius: Theme.borderRadius.full,
381
    marginBottom: 4,
382
    overflow: "hidden",
383
  },
384
  progressFill: {
385
    height: "100%",
386
    backgroundColor: Theme.colors.light.tint,
387
    borderRadius: Theme.borderRadius.full,
388
  },
389
  // Annotation
390
  annotation: {
391
    fontSize: 14,
392
    lineHeight: 22,
393
    color: Colors.light.textSecondary,
394
    fontStyle: "italic",
395
    padding: 4,
396
  },
397
  deleteInfo: {
398
    marginTop: 4,
399
  },
400
  // Linked trigger cards
401
  linkCard: {
402
    padding: Spacing.cardGap,
403
    overflow: "hidden",
404
    gap: 10,
405
  },
406
  linkIconCircle: {
407
    width: 44,
408
    height: 44,
409
    borderRadius: 22,
410
    alignItems: "center",
411
    justifyContent: "center",
412
  },
413
  linkTime: {
414
    fontSize: 12,
415
    color: Colors.light.textSecondary,
416
  },
417
  linkLabel: {
418
    fontSize: 15,
419
    fontWeight: "700",
420
    color: Colors.light.text,
421
  },
422
  impactBar: {
423
    height: 3,
424
    backgroundColor: Colors.light.divider,
425
    borderRadius: 99,
426
    overflow: "hidden",
427
  },
428
  impactFill: {
429
    height: "100%",
430
    borderRadius: 99,
431
  },
432

433
});